import {
  BufferGeometryLoader,
  CanvasTexture,
  ClampToEdgeWrapping,
  Color,
  DirectionalLight,
  FileLoader,
  Line,
  LinearFilter,
  LineBasicMaterial,
  Loader,
  Matrix4,
  Mesh,
  MeshStandardMaterial,
  Object3D,
  PointLight,
  Points,
  PointsMaterial,
  RectAreaLight,
  SpotLight,
  Sprite,
  SpriteMaterial,
  TextureLoader,
  Vector3
} from '../../three.module.js';

const _taskCache = new WeakMap();

class Rhino3dmLoader extends Loader {

  constructor(manager) {

    super(manager);

    this.libraryPath = '';
    this.libraryPending = null;
    this.libraryBinary = null;
    this.libraryConfig = {};

    this.url = '';

    this.workerLimit = 4;
    this.workerPool = [];
    this.workerNextTaskID = 1;
    this.workerSourceURL = '';
    this.workerConfig = {};

    this.materials = [];
    this.warnings = [];

  }

  setLibraryPath(path) {

    this.libraryPath = path;

    return this;

  }

  setWorkerLimit(workerLimit) {

    this.workerLimit = workerLimit;

    return this;

  }

  load(url, onLoad, onProgress, onError) {

    const loader = new FileLoader(this.manager);

    loader.setPath(this.path);
    loader.setResponseType('arraybuffer');
    loader.setRequestHeader(this.requestHeader);

    this.url = url;

    loader.load(url, (buffer) => {

      // Check for an existing task using this buffer. A transferred buffer cannot be transferred
      // again from this thread.
      if (_taskCache.has(buffer)) {

        const cachedTask = _taskCache.get(buffer);

        return cachedTask.promise.then(onLoad).catch(onError);

      }

      this.decodeObjects(buffer, url)
        .then(result => {

          result.userData.warnings = this.warnings;
          onLoad(result);

        })
        .catch(e => onError(e));

    }, onProgress, onError);

  }

  debug() {

    console.log('Task load: ', this.workerPool.map((worker) => worker._taskLoad));

  }

  decodeObjects(buffer, url) {

    let worker;
    let taskID;

    const taskCost = buffer.byteLength;

    const objectPending = this._getWorker(taskCost)
      .then((_worker) => {

        worker = _worker;
        taskID = this.workerNextTaskID++;

        return new Promise((resolve, reject) => {

          worker._callbacks[taskID] = {resolve, reject};

          worker.postMessage({type: 'decode', id: taskID, buffer}, [buffer]);

          // this.debug();

        });

      })
      .then((message) => this._createGeometry(message.data))
      .catch(e => {

        throw e;

      });

    // Remove task from the task list.
    // Note: replaced '.finally()' with '.catch().then()' block - iOS 11 support (#19416)
    objectPending
      .catch(() => true)
      .then(() => {

        if (worker && taskID) {

          this._releaseTask(worker, taskID);

          //this.debug();

        }

      });

    // Cache the task result.
    _taskCache.set(buffer, {

      url: url,
      promise: objectPending

    });

    return objectPending;

  }

  parse(data, onLoad, onError) {

    this.decodeObjects(data, '')
      .then(result => {

        result.userData.warnings = this.warnings;
        onLoad(result);

      })
      .catch(e => onError(e));

  }

  _compareMaterials(material) {

    const mat = {};
    mat.name = material.name;
    mat.color = {};
    mat.color.r = material.color.r;
    mat.color.g = material.color.g;
    mat.color.b = material.color.b;
    mat.type = material.type;

    for (let i = 0; i < this.materials.length; i++) {

      const m = this.materials[i];
      const _mat = {};
      _mat.name = m.name;
      _mat.color = {};
      _mat.color.r = m.color.r;
      _mat.color.g = m.color.g;
      _mat.color.b = m.color.b;
      _mat.type = m.type;

      if (JSON.stringify(mat) === JSON.stringify(_mat)) {

        return m;

      }

    }

    this.materials.push(material);

    return material;

  }

  _createMaterial(material) {

    if (material === undefined) {

      return new MeshStandardMaterial({
        color: new Color(1, 1, 1),
        metalness: 0.8,
        name: 'default',
        side: 2
      });

    }

    const _diffuseColor = material.diffuseColor;

    const diffusecolor = new Color(_diffuseColor.r / 255.0, _diffuseColor.g / 255.0, _diffuseColor.b / 255.0);

    if (_diffuseColor.r === 0 && _diffuseColor.g === 0 && _diffuseColor.b === 0) {

      diffusecolor.r = 1;
      diffusecolor.g = 1;
      diffusecolor.b = 1;

    }

    // console.log( material );

    const mat = new MeshStandardMaterial({
      color: diffusecolor,
      name: material.name,
      side: 2,
      transparent: material.transparency > 0 ? true : false,
      opacity: 1.0 - material.transparency
    });

    const textureLoader = new TextureLoader();

    for (let i = 0; i < material.textures.length; i++) {

      const texture = material.textures[i];

      if (texture.image !== null) {

        const map = textureLoader.load(texture.image);

        switch (texture.type) {

          case 'Diffuse':

            mat.map = map;

            break;

          case 'Bump':

            mat.bumpMap = map;

            break;

          case 'Transparency':

            mat.alphaMap = map;
            mat.transparent = true;

            break;

          case 'Emap':

            mat.envMap = map;

            break;

        }

        map.wrapS = texture.wrapU === 0 ? THREE.RepeatWrapping : THREE.ClampToEdgeWrapping;
        map.wrapT = texture.wrapV === 0 ? THREE.RepeatWrapping : THREE.ClampToEdgeWrapping;
        map.repeat.set(texture.repeat[0], texture.repeat[1]);

      }

    }

    return mat;

  }

  _createGeometry(data) {

    // console.log(data);

    const object = new Object3D();
    const instanceDefinitionObjects = [];
    const instanceDefinitions = [];
    const instanceReferences = [];

    object.userData['layers'] = data.layers;
    object.userData['groups'] = data.groups;
    object.userData['settings'] = data.settings;
    object.userData['objectType'] = 'File3dm';
    object.userData['materials'] = null;
    object.name = this.url;

    let objects = data.objects;
    const materials = data.materials;

    for (let i = 0; i < objects.length; i++) {

      const obj = objects[i];
      const attributes = obj.attributes;

      switch (obj.objectType) {

        case 'InstanceDefinition':

          instanceDefinitions.push(obj);

          break;

        case 'InstanceReference':

          instanceReferences.push(obj);

          break;

        default:

          let _object;

          if (attributes.materialIndex >= 0) {

            const rMaterial = materials[attributes.materialIndex];
            let material = this._createMaterial(rMaterial);
            material = this._compareMaterials(material);
            _object = this._createObject(obj, material);

          } else {

            const material = this._createMaterial();
            _object = this._createObject(obj, material);

          }

          if (_object === undefined) {

            continue;

          }

          const layer = data.layers[attributes.layerIndex];

          _object.visible = layer ? data.layers[attributes.layerIndex].visible : true;

          if (attributes.isInstanceDefinitionObject) {

            instanceDefinitionObjects.push(_object);

          } else {

            object.add(_object);

          }

          break;

      }

    }

    for (let i = 0; i < instanceDefinitions.length; i++) {

      const iDef = instanceDefinitions[i];

      objects = [];

      for (let j = 0; j < iDef.attributes.objectIds.length; j++) {

        const objId = iDef.attributes.objectIds[j];

        for (let p = 0; p < instanceDefinitionObjects.length; p++) {

          const idoId = instanceDefinitionObjects[p].userData.attributes.id;

          if (objId === idoId) {

            objects.push(instanceDefinitionObjects[p]);

          }

        }

      }

      // Currently clones geometry and does not take advantage of instancing

      for (let j = 0; j < instanceReferences.length; j++) {

        const iRef = instanceReferences[j];

        if (iRef.geometry.parentIdefId === iDef.attributes.id) {

          const iRefObject = new Object3D();
          const xf = iRef.geometry.xform.array;

          const matrix = new Matrix4();
          matrix.set(xf[0], xf[1], xf[2], xf[3], xf[4], xf[5], xf[6], xf[7], xf[8], xf[9], xf[10], xf[11], xf[12], xf[13], xf[14], xf[15]);

          iRefObject.applyMatrix4(matrix);

          for (let p = 0; p < objects.length; p++) {

            iRefObject.add(objects[p].clone(true));

          }

          object.add(iRefObject);

        }

      }

    }

    object.userData['materials'] = this.materials;
    return object;

  }

  _createObject(obj, mat) {

    const loader = new BufferGeometryLoader();

    const attributes = obj.attributes;

    let geometry, material, _color, color;

    switch (obj.objectType) {

      case 'Point':
      case 'PointSet':

        geometry = loader.parse(obj.geometry);

        if (geometry.attributes.hasOwnProperty('color')) {

          material = new PointsMaterial({vertexColors: true, sizeAttenuation: false, size: 2});

        } else {

          _color = attributes.drawColor;
          color = new Color(_color.r / 255.0, _color.g / 255.0, _color.b / 255.0);
          material = new PointsMaterial({color: color, sizeAttenuation: false, size: 2});

        }

        material = this._compareMaterials(material);

        const points = new Points(geometry, material);
        points.userData['attributes'] = attributes;
        points.userData['objectType'] = obj.objectType;

        if (attributes.name) {

          points.name = attributes.name;

        }

        return points;

      case 'Mesh':
      case 'Extrusion':
      case 'SubD':
      case 'Brep':

        if (obj.geometry === null) return;

        geometry = loader.parse(obj.geometry);

        if (geometry.attributes.hasOwnProperty('color')) {

          mat.vertexColors = true;

        }

        if (mat === null) {

          mat = this._createMaterial();
          mat = this._compareMaterials(mat);

        }

        const mesh = new Mesh(geometry, mat);
        mesh.castShadow = attributes.castsShadows;
        mesh.receiveShadow = attributes.receivesShadows;
        mesh.userData['attributes'] = attributes;
        mesh.userData['objectType'] = obj.objectType;

        if (attributes.name) {

          mesh.name = attributes.name;

        }

        return mesh;

      case 'Curve':

        geometry = loader.parse(obj.geometry);

        _color = attributes.drawColor;
        color = new Color(_color.r / 255.0, _color.g / 255.0, _color.b / 255.0);

        material = new LineBasicMaterial({color: color});
        material = this._compareMaterials(material);

        const lines = new Line(geometry, material);
        lines.userData['attributes'] = attributes;
        lines.userData['objectType'] = obj.objectType;

        if (attributes.name) {

          lines.name = attributes.name;

        }

        return lines;

      case 'TextDot':

        geometry = obj.geometry;

        const ctx = document.createElement('canvas').getContext('2d');
        const font = `${geometry.fontHeight}px ${geometry.fontFace}`;
        ctx.font = font;
        const width = ctx.measureText(geometry.text).width + 10;
        const height = geometry.fontHeight + 10;

        const r = window.devicePixelRatio;

        ctx.canvas.width = width * r;
        ctx.canvas.height = height * r;
        ctx.canvas.style.width = width + 'px';
        ctx.canvas.style.height = height + 'px';
        ctx.setTransform(r, 0, 0, r, 0, 0);

        ctx.font = font;
        ctx.textBaseline = 'middle';
        ctx.textAlign = 'center';
        color = attributes.drawColor;
        ctx.fillStyle = `rgba(${color.r},${color.g},${color.b},${color.a})`;
        ctx.fillRect(0, 0, width, height);
        ctx.fillStyle = 'white';
        ctx.fillText(geometry.text, width / 2, height / 2);

        const texture = new CanvasTexture(ctx.canvas);
        texture.minFilter = LinearFilter;
        texture.wrapS = ClampToEdgeWrapping;
        texture.wrapT = ClampToEdgeWrapping;

        material = new SpriteMaterial({map: texture, depthTest: false});
        const sprite = new Sprite(material);
        sprite.position.set(geometry.point[0], geometry.point[1], geometry.point[2]);
        sprite.scale.set(width / 10, height / 10, 1.0);

        sprite.userData['attributes'] = attributes;
        sprite.userData['objectType'] = obj.objectType;

        if (attributes.name) {

          sprite.name = attributes.name;

        }

        return sprite;

      case 'Light':

        geometry = obj.geometry;

        let light;

        switch (geometry.lightStyle.name) {

          case 'LightStyle_WorldPoint':

            light = new PointLight();
            light.castShadow = attributes.castsShadows;
            light.position.set(geometry.location[0], geometry.location[1], geometry.location[2]);
            light.shadow.normalBias = 0.1;

            break;

          case 'LightStyle_WorldSpot':

            light = new SpotLight();
            light.castShadow = attributes.castsShadows;
            light.position.set(geometry.location[0], geometry.location[1], geometry.location[2]);
            light.target.position.set(geometry.direction[0], geometry.direction[1], geometry.direction[2]);
            light.angle = geometry.spotAngleRadians;
            light.shadow.normalBias = 0.1;

            break;

          case 'LightStyle_WorldRectangular':

            light = new RectAreaLight();
            const width = Math.abs(geometry.width[2]);
            const height = Math.abs(geometry.length[0]);
            light.position.set(geometry.location[0] - (height / 2), geometry.location[1], geometry.location[2] - (width / 2));
            light.height = height;
            light.width = width;
            light.lookAt(new Vector3(geometry.direction[0], geometry.direction[1], geometry.direction[2]));

            break;

          case 'LightStyle_WorldDirectional':

            light = new DirectionalLight();
            light.castShadow = attributes.castsShadows;
            light.position.set(geometry.location[0], geometry.location[1], geometry.location[2]);
            light.target.position.set(geometry.direction[0], geometry.direction[1], geometry.direction[2]);
            light.shadow.normalBias = 0.1;

            break;

          case 'LightStyle_WorldLinear':
            // not conversion exists, warning has already been printed to the console
            break;

          default:
            break;

        }

        if (light) {

          light.intensity = geometry.intensity;
          _color = geometry.diffuse;
          color = new Color(_color.r / 255.0, _color.g / 255.0, _color.b / 255.0);
          light.color = color;
          light.userData['attributes'] = attributes;
          light.userData['objectType'] = obj.objectType;

        }

        return light;

    }

  }

  _initLibrary() {

    if (!this.libraryPending) {

      // Load rhino3dm wrapper.
      const jsLoader = new FileLoader(this.manager);
      jsLoader.setPath(this.libraryPath);
      const jsContent = new Promise((resolve, reject) => {

        jsLoader.load('rhino3dm.js', resolve, undefined, reject);

      });

      // Load rhino3dm WASM binary.
      const binaryLoader = new FileLoader(this.manager);
      binaryLoader.setPath(this.libraryPath);
      binaryLoader.setResponseType('arraybuffer');
      const binaryContent = new Promise((resolve, reject) => {

        binaryLoader.load('rhino3dm.wasm', resolve, undefined, reject);

      });

      this.libraryPending = Promise.all([jsContent, binaryContent])
        .then(([jsContent, binaryContent]) => {

          //this.libraryBinary = binaryContent;
          this.libraryConfig.wasmBinary = binaryContent;

          const fn = Rhino3dmWorker.toString();

          const body = [
            '/* rhino3dm.js */',
            jsContent,
            '/* worker */',
            fn.substring(fn.indexOf('{') + 1, fn.lastIndexOf('}'))
          ].join('\n');

          this.workerSourceURL = URL.createObjectURL(new Blob([body]));

        });

    }

    return this.libraryPending;

  }

  _getWorker(taskCost) {

    return this._initLibrary().then(() => {

      if (this.workerPool.length < this.workerLimit) {

        const worker = new Worker(this.workerSourceURL);

        worker._callbacks = {};
        worker._taskCosts = {};
        worker._taskLoad = 0;

        worker.postMessage({
          type: 'init',
          libraryConfig: this.libraryConfig
        });

        worker.onmessage = e => {

          const message = e.data;

          switch (message.type) {

            case 'warning':
              this.warnings.push(message.data);
              console.warn(message.data);
              break;

            case 'decode':
              worker._callbacks[message.id].resolve(message);
              break;

            case 'error':
              worker._callbacks[message.id].reject(message);
              break;

            default:
              console.error('THREE.Rhino3dmLoader: Unexpected message, "' + message.type + '"');

          }

        };

        this.workerPool.push(worker);

      } else {

        this.workerPool.sort(function (a, b) {

          return a._taskLoad > b._taskLoad ? -1 : 1;

        });

      }

      const worker = this.workerPool[this.workerPool.length - 1];

      worker._taskLoad += taskCost;

      return worker;

    });

  }

  _releaseTask(worker, taskID) {

    worker._taskLoad -= worker._taskCosts[taskID];
    delete worker._callbacks[taskID];
    delete worker._taskCosts[taskID];

  }

  dispose() {

    for (let i = 0; i < this.workerPool.length; ++i) {

      this.workerPool[i].terminate();

    }

    this.workerPool.length = 0;

    return this;

  }

}

/* WEB WORKER */

function Rhino3dmWorker() {

  let libraryPending;
  let libraryConfig;
  let rhino;
  let taskID;

  onmessage = function (e) {

    const message = e.data;

    switch (message.type) {

      case 'init':

        // console.log(message)
        libraryConfig = message.libraryConfig;
        const wasmBinary = libraryConfig.wasmBinary;
        let RhinoModule;
        libraryPending = new Promise(function (resolve) {

          /* Like Basis Loader */
          RhinoModule = {wasmBinary, onRuntimeInitialized: resolve};

          rhino3dm(RhinoModule); // eslint-disable-line no-undef

        }).then(() => {

          rhino = RhinoModule;

        });

        break;

      case 'decode':

        taskID = message.id;
        const buffer = message.buffer;
        libraryPending.then(() => {

          try {

            const data = decodeObjects(rhino, buffer);
            self.postMessage({type: 'decode', id: message.id, data});

          } catch (error) {

            self.postMessage({type: 'error', id: message.id, error});

          }

        });

        break;

    }

  };

  function decodeObjects(rhino, buffer) {

    const arr = new Uint8Array(buffer);
    const doc = rhino.File3dm.fromByteArray(arr);

    const objects = [];
    const materials = [];
    const layers = [];
    const views = [];
    const namedViews = [];
    const groups = [];
    const strings = [];

    //Handle objects

    const objs = doc.objects();
    const cnt = objs.count;

    for (let i = 0; i < cnt; i++) {

      const _object = objs.get(i);

      const object = extractObjectData(_object, doc);

      _object.delete();

      if (object) {

        objects.push(object);

      }

    }

    // Handle instance definitions
    // console.log( `Instance Definitions Count: ${doc.instanceDefinitions().count()}` );

    for (let i = 0; i < doc.instanceDefinitions().count(); i++) {

      const idef = doc.instanceDefinitions().get(i);
      const idefAttributes = extractProperties(idef);
      idefAttributes.objectIds = idef.getObjectIds();

      objects.push({geometry: null, attributes: idefAttributes, objectType: 'InstanceDefinition'});

    }

    // Handle materials

    const textureTypes = [
      // rhino.TextureType.Bitmap,
      rhino.TextureType.Diffuse,
      rhino.TextureType.Bump,
      rhino.TextureType.Transparency,
      rhino.TextureType.Opacity,
      rhino.TextureType.Emap
    ];

    const pbrTextureTypes = [
      rhino.TextureType.PBR_BaseColor,
      rhino.TextureType.PBR_Subsurface,
      rhino.TextureType.PBR_SubsurfaceScattering,
      rhino.TextureType.PBR_SubsurfaceScatteringRadius,
      rhino.TextureType.PBR_Metallic,
      rhino.TextureType.PBR_Specular,
      rhino.TextureType.PBR_SpecularTint,
      rhino.TextureType.PBR_Roughness,
      rhino.TextureType.PBR_Anisotropic,
      rhino.TextureType.PBR_Anisotropic_Rotation,
      rhino.TextureType.PBR_Sheen,
      rhino.TextureType.PBR_SheenTint,
      rhino.TextureType.PBR_Clearcoat,
      rhino.TextureType.PBR_ClearcoatBump,
      rhino.TextureType.PBR_ClearcoatRoughness,
      rhino.TextureType.PBR_OpacityIor,
      rhino.TextureType.PBR_OpacityRoughness,
      rhino.TextureType.PBR_Emission,
      rhino.TextureType.PBR_AmbientOcclusion,
      rhino.TextureType.PBR_Displacement
    ];

    for (let i = 0; i < doc.materials().count(); i++) {

      const _material = doc.materials().get(i);
      const _pbrMaterial = _material.physicallyBased();

      let material = extractProperties(_material);

      const textures = [];

      for (let j = 0; j < textureTypes.length; j++) {

        const _texture = _material.getTexture(textureTypes[j]);
        if (_texture) {

          let textureType = textureTypes[j].constructor.name;
          textureType = textureType.substring(12, textureType.length);
          const texture = {type: textureType};

          const image = doc.getEmbeddedFileAsBase64(_texture.fileName);

          texture.wrapU = _texture.wrapU;
          texture.wrapV = _texture.wrapV;
          texture.wrapW = _texture.wrapW;
          const uvw = _texture.uvwTransform.toFloatArray(true);
          texture.repeat = [uvw[0], uvw[5]];

          if (image) {

            texture.image = 'data:image/png;base64,' + image;

          } else {

            self.postMessage({
              type: 'warning', id: taskID, data: {
                message: `THREE.3DMLoader: Image for ${textureType} texture not embedded in file.`,
                type: 'missing resource'
              }

            });

            texture.image = null;

          }

          textures.push(texture);

          _texture.delete();

        }

      }

      material.textures = textures;

      if (_pbrMaterial.supported) {

        for (let j = 0; j < pbrTextureTypes.length; j++) {

          const _texture = _material.getTexture(pbrTextureTypes[j]);
          if (_texture) {

            const image = doc.getEmbeddedFileAsBase64(_texture.fileName);
            let textureType = pbrTextureTypes[j].constructor.name;
            textureType = textureType.substring(12, textureType.length);
            const texture = {type: textureType, image: 'data:image/png;base64,' + image};
            textures.push(texture);

            _texture.delete();

          }

        }

        const pbMaterialProperties = extractProperties(_material.physicallyBased());

        material = Object.assign(pbMaterialProperties, material);

      }

      materials.push(material);

      _material.delete();
      _pbrMaterial.delete();

    }

    // Handle layers

    for (let i = 0; i < doc.layers().count(); i++) {

      const _layer = doc.layers().get(i);
      const layer = extractProperties(_layer);

      layers.push(layer);

      _layer.delete();

    }

    // Handle views

    for (let i = 0; i < doc.views().count(); i++) {

      const _view = doc.views().get(i);
      const view = extractProperties(_view);

      views.push(view);

      _view.delete();

    }

    // Handle named views

    for (let i = 0; i < doc.namedViews().count(); i++) {

      const _namedView = doc.namedViews().get(i);
      const namedView = extractProperties(_namedView);

      namedViews.push(namedView);

      _namedView.delete();

    }

    // Handle groups

    for (let i = 0; i < doc.groups().count(); i++) {

      const _group = doc.groups().get(i);
      const group = extractProperties(_group);

      groups.push(group);

      _group.delete();

    }

    // Handle settings

    const settings = extractProperties(doc.settings());

    //TODO: Handle other document stuff like dimstyles, instance definitions, bitmaps etc.

    // Handle dimstyles
    // console.log( `Dimstyle Count: ${doc.dimstyles().count()}` );

    // Handle bitmaps
    // console.log( `Bitmap Count: ${doc.bitmaps().count()}` );

    // Handle strings
    // console.log( `Document Strings Count: ${doc.strings().count()}` );
    // Note: doc.strings().documentUserTextCount() counts any doc.strings defined in a section
    //console.log( `Document User Text Count: ${doc.strings().documentUserTextCount()}` );

    const strings_count = doc.strings().count();

    for (let i = 0; i < strings_count; i++) {

      strings.push(doc.strings().get(i));

    }

    doc.delete();

    return {objects, materials, layers, views, namedViews, groups, strings, settings};

  }

  function extractObjectData(object, doc) {

    const _geometry = object.geometry();
    const _attributes = object.attributes();
    let objectType = _geometry.objectType;
    let geometry, attributes, position, data, mesh;

    // skip instance definition objects
    //if( _attributes.isInstanceDefinitionObject ) { continue; }

    // TODO: handle other geometry types
    switch (objectType) {

      case rhino.ObjectType.Curve:

        const pts = curveToPoints(_geometry, 100);

        position = {};
        attributes = {};
        data = {};

        position.itemSize = 3;
        position.type = 'Float32Array';
        position.array = [];

        for (let j = 0; j < pts.length; j++) {

          position.array.push(pts[j][0]);
          position.array.push(pts[j][1]);
          position.array.push(pts[j][2]);

        }

        attributes.position = position;
        data.attributes = attributes;

        geometry = {data};

        break;

      case rhino.ObjectType.Point:

        const pt = _geometry.location;

        position = {};
        const color = {};
        attributes = {};
        data = {};

        position.itemSize = 3;
        position.type = 'Float32Array';
        position.array = [pt[0], pt[1], pt[2]];

        const _color = _attributes.drawColor(doc);

        color.itemSize = 3;
        color.type = 'Float32Array';
        color.array = [_color.r / 255.0, _color.g / 255.0, _color.b / 255.0];

        attributes.position = position;
        attributes.color = color;
        data.attributes = attributes;

        geometry = {data};

        break;

      case rhino.ObjectType.PointSet:
      case rhino.ObjectType.Mesh:

        geometry = _geometry.toThreejsJSON();

        break;

      case rhino.ObjectType.Brep:

        const faces = _geometry.faces();
        mesh = new rhino.Mesh();

        for (let faceIndex = 0; faceIndex < faces.count; faceIndex++) {

          const face = faces.get(faceIndex);
          const _mesh = face.getMesh(rhino.MeshType.Any);

          if (_mesh) {

            mesh.append(_mesh);
            _mesh.delete();

          }

          face.delete();

        }

        if (mesh.faces().count > 0) {

          mesh.compact();
          geometry = mesh.toThreejsJSON();
          faces.delete();

        }

        mesh.delete();

        break;

      case rhino.ObjectType.Extrusion:

        mesh = _geometry.getMesh(rhino.MeshType.Any);

        if (mesh) {

          geometry = mesh.toThreejsJSON();
          mesh.delete();

        }

        break;

      case rhino.ObjectType.TextDot:

        geometry = extractProperties(_geometry);

        break;

      case rhino.ObjectType.Light:

        geometry = extractProperties(_geometry);

        if (geometry.lightStyle.name === 'LightStyle_WorldLinear') {

          self.postMessage({
            type: 'warning', id: taskID, data: {
              message: `THREE.3DMLoader: No conversion exists for ${objectType.constructor.name} ${geometry.lightStyle.name}`,
              type: 'no conversion',
              guid: _attributes.id
            }

          });

        }

        break;

      case rhino.ObjectType.InstanceReference:

        geometry = extractProperties(_geometry);
        geometry.xform = extractProperties(_geometry.xform);
        geometry.xform.array = _geometry.xform.toFloatArray(true);

        break;

      case rhino.ObjectType.SubD:

        // TODO: precalculate resulting vertices and faces and warn on excessive results
        _geometry.subdivide(3);
        mesh = rhino.Mesh.createFromSubDControlNet(_geometry);
        if (mesh) {

          geometry = mesh.toThreejsJSON();
          mesh.delete();

        }

        break;

      /*
      case rhino.ObjectType.Annotation:
      case rhino.ObjectType.Hatch:
      case rhino.ObjectType.ClipPlane:
      */

      default:

        self.postMessage({
          type: 'warning', id: taskID, data: {
            message: `THREE.3DMLoader: Conversion not implemented for ${objectType.constructor.name}`,
            type: 'not implemented',
            guid: _attributes.id
          }

        });

        break;

    }

    if (geometry) {

      attributes = extractProperties(_attributes);
      attributes.geometry = extractProperties(_geometry);

      if (_attributes.groupCount > 0) {

        attributes.groupIds = _attributes.getGroupList();

      }

      if (_attributes.userStringCount > 0) {

        attributes.userStrings = _attributes.getUserStrings();

      }

      if (_geometry.userStringCount > 0) {

        attributes.geometry.userStrings = _geometry.getUserStrings();

      }

      attributes.drawColor = _attributes.drawColor(doc);

      objectType = objectType.constructor.name;
      objectType = objectType.substring(11, objectType.length);

      return {geometry, attributes, objectType};

    } else {

      self.postMessage({
        type: 'warning', id: taskID, data: {
          message: `THREE.3DMLoader: ${objectType.constructor.name} has no associated mesh geometry.`,
          type: 'missing mesh',
          guid: _attributes.id
        }

      });

    }

  }

  function extractProperties(object) {

    const result = {};

    for (const property in object) {

      const value = object[property];

      if (typeof value !== 'function') {

        if (typeof value === 'object' && value !== null && value.hasOwnProperty('constructor')) {

          result[property] = {name: value.constructor.name, value: value.value};

        } else {

          result[property] = value;

        }

      } else {

        // these are functions that could be called to extract more data.
        //console.log( `${property}: ${object[ property ].constructor.name}` );

      }

    }

    return result;

  }

  function curveToPoints(curve, pointLimit) {

    let pointCount = pointLimit;
    let rc = [];
    const ts = [];

    if (curve instanceof rhino.LineCurve) {

      return [curve.pointAtStart, curve.pointAtEnd];

    }

    if (curve instanceof rhino.PolylineCurve) {

      pointCount = curve.pointCount;
      for (let i = 0; i < pointCount; i++) {

        rc.push(curve.point(i));

      }

      return rc;

    }

    if (curve instanceof rhino.PolyCurve) {

      const segmentCount = curve.segmentCount;

      for (let i = 0; i < segmentCount; i++) {

        const segment = curve.segmentCurve(i);
        const segmentArray = curveToPoints(segment, pointCount);
        rc = rc.concat(segmentArray);
        segment.delete();

      }

      return rc;

    }

    if (curve instanceof rhino.ArcCurve) {

      pointCount = Math.floor(curve.angleDegrees / 5);
      pointCount = pointCount < 2 ? 2 : pointCount;
      // alternative to this hardcoded version: https://stackoverflow.com/a/18499923/2179399

    }

    if (curve instanceof rhino.NurbsCurve && curve.degree === 1) {

      const pLine = curve.tryGetPolyline();

      for (let i = 0; i < pLine.count; i++) {

        rc.push(pLine.get(i));

      }

      pLine.delete();

      return rc;

    }

    const domain = curve.domain;
    const divisions = pointCount - 1.0;

    for (let j = 0; j < pointCount; j++) {

      const t = domain[0] + (j / divisions) * (domain[1] - domain[0]);

      if (t === domain[0] || t === domain[1]) {

        ts.push(t);
        continue;

      }

      const tan = curve.tangentAt(t);
      const prevTan = curve.tangentAt(ts.slice(-1)[0]);

      // Duplicated from THREE.Vector3
      // How to pass imports to worker?

      const tS = tan[0] * tan[0] + tan[1] * tan[1] + tan[2] * tan[2];
      const ptS = prevTan[0] * prevTan[0] + prevTan[1] * prevTan[1] + prevTan[2] * prevTan[2];

      const denominator = Math.sqrt(tS * ptS);

      let angle;

      if (denominator === 0) {

        angle = Math.PI / 2;

      } else {

        const theta = (tan.x * prevTan.x + tan.y * prevTan.y + tan.z * prevTan.z) / denominator;
        angle = Math.acos(Math.max(-1, Math.min(1, theta)));

      }

      if (angle < 0.1) continue;

      ts.push(t);

    }

    rc = ts.map(t => curve.pointAt(t));
    return rc;

  }

}

export {Rhino3dmLoader};
